Skip to content

S08-04 Node-内置模块

[TOC]

path

API-path

方法

  • path.dirname()(p),获取文件目录
  • path.basename()(p, extension?),获取文件名
  • path.extname()(p),获取文件扩展名
  • path.join()(...paths?),用于将多个路径片段拼接成一个完整的路径(解析./../)。
  • path.resolve()(...paths?),将多个路径拼接(解析./../),返回一个绝对路径。

概述

作用:用于对路径和文件进行处理

痛点:mac、linux、windows 上路径分隔符格式不同

  • windows:使用 \或者 \\ 来作为文件路径的分隔符,目前也支持 /
  • Mac、Linux、Unix:使用 / 来作为文件路径的分隔符;

解决:为了屏蔽他们之间的差异,在开发中对于路径的操作我们可以使用 path 模块

fs

API-fs

fs

  • fs.readFile()(path, options?, callback)回调Promise,从文件中读取数据。
  • fs.writeFile()(file, data, options?, callback)回调Promise写入数据到文件中。
  • fs.open()(path, flags?, mode?, callback)打开文件
  • fs.close()(fd, callback?),关闭一个已经打开的文件
  • fs.rename()(oldPath, newPath, callback)重命名文件或目录。
  • fs.stat()(path, options?, callback),获取文件或目录的详细信息。
  • fs.mkdir()(path, options?, callback)创建一个新的目录
  • fs.readdir()(path, options?, callback)读取指定目录中的所有文件和子目录。
  • fs.fstat()(fd, options?, callback),通过 fd 获取文件或目录的详细信息。

FileHandle

  • FileHandle(),表示一个打开的文件句柄。它提供了与文件描述符相同的功能,可以使用它的方法进行文件的读写和关闭操作。
  • fileHandle.fdinterger,获取到与之关联的文件描述符。
  • fileHandle.read()(buffer, offset?, length?, position?),从文件中读取数据流。
  • fileHandle.write()(buffer, offset?, length?, position?),向文件中写入数据流。
  • fileHandle.close()()关闭文件句柄,释放资源。
  • fileHandle.appendFile()(data, options?),将指定的数据追加到文件末尾,如果文件不存在,则会创建文件。

fs.Dirent

  • fs.Dirent(),表示文件或目录的信息。当使用 fs.readdir() 函数的 withFileTypes 选项时,返回的数组中的每个元素都是 fs.Dirent 对象,其中包含文件或目录的信息。
  • dirent.isFile()(),检查当前目录项(dirent)是否是一个文件
  • dirent.isDirectory()(),检查当前对象是否是一个目录

fs.Stats

概述

概念:fs 是 File System 的缩写,表示文件系统

痛点:任何服务器端语言都需要有自己的文件系统,方便将各种数据、文件等放到不同的地方

作用:提供了一组功能丰富的方法来处理文件系统操作。包括创建、读取、写入和操作文件和目录

特点

  • 跨系统:借助 Node 的文件系统,可以在任何的操作系统(window、Mac、Linux)上直接操作文件

文档https://nodejs.org/dist/latest-v14.x/docs/api/fs.html

API 操作方式

  • 同步操作:代码会被阻塞,后续代码不会继续执行
  • 异步操作-回调函数:代码不会被阻塞,需要传入回调函数,当获取到结果时,调用回调函数
  • 异步操作-Promise:代码不会被阻塞,通过 fs.promises 调用方法操作,会返回一个 Promise,可以通过 then, catch 处理结果

基本使用

这里以获取一个文件的状态为例:

注意:需要提前引入 fs 模块;

方式一:同步操作文件

js
// 1.方式一: 同步读取文件
const state = fs.statSync('../foo.txt')
console.log(state)

console.log('后续代码执行')

方式二:异步回调函数操作文件

js
// 2.方式二: 异步读取
fs.stat('../foo.txt', (err, state) => {
  if (err) {
    console.log(err)
    return
  }
  console.log(state)
})
console.log('后续代码执行')

方式三:异步 Promise 操作文件

js
// 3.方式三: Promise方式
fs.promises
  .stat('../foo.txt')
  .then((state) => {
    console.log(state)
  })
  .catch((err) => {
    console.log(err)
  })
console.log('后续代码执行')

文件描述符

概念:文件描述符(File descriptors):在 POSIX 系统上,对于每个进程,内核都维护着一张当前打开着的文件和资源的表格。每个打开的文件都分配了一个称为文件描述符的简单的数字标识符。在系统层,所有文件系统操作都使用这些文件描述符来标识和跟踪每个特定的文件。Windows 系统使用了一个虽然不同但概念上类似的机制来跟踪资源。为了简化用户的工作,Node.js 抽象出操作系统之间的特定差异,并为所有打开的文件分配一个数字型的文件描述符

作用

  • fs.readFile()(path, options?, callback)回调Promise,从文件中读取数据。
  • fs.writeFile()(file, data, options?, callback)回调Promise写入数据到文件中。
  • fs.fstat()(fd, options?, callback),通过 fd 获取文件或目录的详细信息。
  • fs.open()(path, flags?, mode?, callback)打开文件
js
// 获取文件描述符
fs.open('../foo.txt', 'r', (err, fd) => {
  console.log(fd)

  fs.fstat(fd, (err, state) => {
    console.log(state)
  })
})

flags

说明: 打开文件的方式

选项

  • w写入默认值,打开文件写入
  • w+:打开文件进行写+读,如果不存在则创建文件
  • r读取默认值,打开文件读取
  • r+:打开文件进行读+写,如果不存在那么抛出异常
  • a:打开要写入的文件,将流追加到文件末尾。如果不存在则创建文件
  • a+:打开文件以进行写+读,将流追加到文件末尾。如果不存在则创建文件

events

API-events

EventEmitter

概述

作用events提供了用于处理事件的基本功能

基本使用

Node 中的核心 API 都是基于异步事件驱动的:

  • 在这个体系中,某些对象(发射器(Emitters))发出某一个事件;
  • 我们可以监听这个事件(监听器 Listeners),并且传入的回调函数,这个回调函数会在监听到事件时调用;

发出事件和监听事件都是通过 EventEmitter 类来完成的,它们都属于 events 对象。

js
const EventEmiter = require('events')

// 监听事件
const bus = new EventEmiter()

function clickHanlde(args) {
  console.log('监听到click事件', args)
}

bus.on('click', clickHanlde)

setTimeout(() => {
  bus.emit('click', 'coderwhy')
  bus.off('click', clickHanlde)
  bus.emit('click', 'kobe')
}, 2000)

常见的方法

EventEmitter 的实例有一些属性,可以记录一些信息:

  • emitter.eventNames()(),返回当前 emitter 对象注册的事件名数组
  • emitter.getMaxListeners()()默认:10,返回当前 emitter 对象的最大监听器数量,可以通过setMaxListeners()来修改。
  • emitter.listenerCount()(eventName),返回当前 emitter 对象某一个事件的监听器的个数
  • emitter.listeners()(eventName),返回当前 emitter 对象某个事件监听器上所有的监听器数组

示例:常见的方法

image-20251018153427698

Buffer

API-Buffer

Buffer

静态方法

实例方法

概述

二进制

计算机中所有的内容:文字、数字、图片、音频、视频最终都会使用二进制来表示。

JavaScript 可以直接去处理非常直观的数据:比如字符串,我们通常展示给用户的也是这些内容。

图片的处理

不对啊,JavaScript 不是也可以处理图片吗?

  • 网页端

    事实上在网页端,图片我们一直是交给浏览器来处理的:

    • JS 或者 HTML,只是负责告诉浏览器一个图片的地址;
    • 浏览器负责获取这个图片,并且最终将这个图片渲染出来;
  • 服务端

    但是对于服务器来说是不一样的,服务器要处理的本地文件类型相对较多:

    • 文本类型

      文本文件并不是使用 utf-8进行编码的,而是用 GBK,那么我们必须读取到他们的二进制数据,再通过 GKB 转换成对应的文字。

    • 图片类型

      我们需要读取的是一张图片数据(二进制),再通过某些手段对图片数据进行二次的处理(裁剪、格式转换、旋转、添加滤镜),Node 中有一个 Sharp 库就是读取图片或者传入图片的 Buffer 对其再进行处理。

    • 流类型

      比如在 Node 中通过 TCP 建立长连接,TCP 传输的是字节流,我们需要将数据转成字节再进行传入,并且需要知道传输字节的大小(客服端需要根据大小来判断读取多少内容)。

我们会发现,对于前端开发来说,通常很少会和二进制打交道,但是对于服务器端为了做很多的功能,我们必须直接去操作其二进制的数据。

所以 Node 为了可以方便开发者完成更多功能,提供给了我们一个类 Buffer,并且它是全局的。

Buffer 和二进制

我们前面说过,Buffer 中存储的是二进制数据,那么到底是如何存储呢?

  • 我们可以将 Buffer 看成是一个存储二进制的数组;
  • 这个数组中的每一项,可以保存 8 位二进制:0000 0000

为什么是 8 位呢?

  • 在计算机中,很少的情况我们会直接操作一位二进制,因为一位二进制存储的数据是非常有限的;
  • 所以通常会将 8 位合在一起作为一个单元,这个单元称之为一个字节(byte);
  • 也就是说 1byte = 8bit1kb=1024byte1M=1024kb;
  • 比如很多编程语言中的 int 类型是 4 个字节,long 类型是 8 个字节;
  • 比如 TCP 传输的是字节流,在写入和读取时都需要说明字节的个数;
  • 比如 RGB 的值分别都是 255,所以本质上在计算机中都是用一个字节存储的;

Buffer 和字符串

也就是说,Buffer 相当于是一个字节的数组,数组中的每一项对应一个字节的大小:

如果我们希望将一个字符串放入到 Buffer 中,是怎么样的过程呢?

js
const buffer01 = new Buffer('why')

console.log(buffer01)

字符串存储 buffer 的过程

image-20240719155527718

当然目前已经不希望我们这样来做了:

VSCode 的警告

image-20240719155541582

那么我们可以通过另外一个创建方法:

js
const buffer2 = Buffer.from('why')
console.log(buffer2)

如果是中文呢?

js
const buffer3 = Buffer.from('王红元')
console.log(buffer3) // <Buffer e7 8e 8b e7 ba a2 e5 85 83>
const str = buffer3.toString()
console.log(str) // 王红元

如果编码和解码不同:会显示乱码

js
const buffer3 = Buffer.from('王红元', 'utf16le') // 编码
console.log(buffer3)

const str = buffer3.toString('utf8') // 解码
console.log(str) // �s�~CQ

常见方法

创建 Buffer

Buffer 的创建方式有很多:

image-20251018165720387

buffer 的创建

来看一下Buffer.alloc:

  • 我们会发现创建了一个 8 位长度的 Buffer,里面所有的数据默认为 00;
js
const buffer01 = Buffer.alloc(8)

console.log(buffer01) // <Buffer 00 00 00 00 00 00 00 00>

我们也可以对其进行操作:

js
buffer01[0] = 'w'.charCodeAt()
buffer01[1] = 100
buffer01[2] = 0x66
console.log(buffer01)

也可以使用相同的方式来获取:

js
console.log(buffer01[0])
console.log(buffer01[0].toString(16))

Buffer 和文件读取

文本文件的读取:

js
const fs = require('fs')

fs.readFile('./test.txt', (err, data) => {
  console.log(data) // <Buffer 48 65 6c 6c 6f 20 57 6f 72 6c 64>
  console.log(data.toString()) // Hello World
})

图片文件的读取:

js
fs.readFile('./zznh.jpg', (err, data) => {
  console.log(data) // <Buffer ff d8 ff e0 ... 40418 more bytes>
})

图片文件的读取和转换:

  • 将读取的某一张图片,转换成一张 200x200 的图片;
  • 这里我们可以借助于 sharp 库来完成;
js
const sharp = require('sharp')
const fs = require('fs')

sharp('./test.png')
  .resize(1000, 1000)
  .toBuffer()
  .then((data) => {
    fs.writeFileSync('./test_copy.png', data)
  })

Buffer 内存分配

事实上我们创建 Buffer 时,并不会频繁的向操作系统申请内存,它会默认先申请一个 8 * 1024 个字节大小的内存,也就是 8kb

node 源码:

js
// node/lib/buffer.js:135行
Buffer.poolSize = 8 * 1024
let poolSize, poolOffset, allocPool

const encodingsMap = ObjectCreate(null)
for (let i = 0; i < encodings.length; ++i) encodingsMap[encodings[i]] = i

function createPool() {
  poolSize = Buffer.poolSize
  allocPool = createUnsafeBuffer(poolSize).buffer
  markAsUntransferable(allocPool)
  poolOffset = 0
}
createPool()

假如我们调用 Buffer.from 申请 Buffer:

这里我们以从字符串创建为例

js
// node/lib/buffer.js:290行
Buffer.from = function from(value, encodingOrOffset, length) {
  if (typeof value === 'string') return fromString(value, encodingOrOffset)

  // 如果是对象,另外一种处理情况
  // ...
}

我们查看 fromString 的调用:

js
// node/lib/buffer.js:428行
function fromString(string, encoding) {
  let ops
  if (typeof encoding !== 'string' || encoding.length === 0) {
    if (string.length === 0) return new FastBuffer()
    ops = encodingOps.utf8
    encoding = undefined
  } else {
    ops = getEncodingOps(encoding)
    if (ops === undefined) throw new ERR_UNKNOWN_ENCODING(encoding)
    if (string.length === 0) return new FastBuffer()
  }
  return fromStringFast(string, ops)
}

接着我们查看 fromStringFast:

  • 这里做的事情是判断剩余的长度是否还足够填充这个字符串;
  • 如果不足够,那么就要通过 createPool 创建新的空间;
  • 如果够就直接使用,但是之后要进行 poolOffset的偏移变化;
js
// node/lib/buffer.js:428行
function fromStringFast(string, ops) {
  const length = ops.byteLength(string)

  if (length >= Buffer.poolSize >>> 1) return createFromString(string, ops.encodingVal)

  if (length > poolSize - poolOffset) createPool()
  let b = new FastBuffer(allocPool, poolOffset, length)
  const actual = ops.write(b, string, 0, length)
  if (actual !== length) {
    // byteLength() may overestimate. That's a rare case, though.
    b = new FastBuffer(allocPool, poolOffset, actual)
  }
  poolOffset += actual
  alignPool()
  return b
}

stream

API-stream

stream.Readable

stream.Writable

概述

定义

  • Stream 是一种处理流式数据的抽象接口
  • Stream 可以将数据分成小块并逐个地进行处理,而无需等待整个数据集到达内存中
  • Stream 可以用于处理各种类型的数据,包括文本、二进制、音频和视频等

注意:所有的流都是 EventEmitter 的实例

作用:通过 stream 读写文件可以控制细节

  • 从什么位置开始读、读到什么位置、一次性读取多少个字节
  • 暂停读取、恢复读取
  • 文件很大时,通过 stream 分批次写入

应用:基于流实现的对象

  • http 模块的 Request、Response 对象
  • process.stdout 对象

流(Stream)的分类

  • Writable:可以向其写入数据的流(例如 fs.createWriteStream())。
  • Readable:可以从中读取数据的流(例如 fs.createReadStream())。
  • Duplex:同时为Readable和的流Writable(例如 net.Socket)。
  • TransformDuplex可以在写入和读取数据时修改或转换数据的流(例如zlib.createDeflate())。

Stream 和 EventEmitter 关系

image-20221216203524308

Readable

一次性读取

之前我们读取一个文件的信息:

js
fs.readFile('./foo.txt', (err, data) => {
  console.log(data)
})

这种方式是一次性将一个文件中所有的内容都读取到程序(内存)中

问题:文件过大、读取的位置、结束的位置、一次读取的大小;

流式读取

这个时候,我们可以使用 createReadStream(),我们来看几个参数,更多参数可以参考官网:

  • start:文件读取开始的位置;
  • end:文件读取结束的位置;
  • highWaterMark:一次性读取字节的长度,默认是 64kb;

基本使用

1、创建读取流

js
const read = fs.createReadStream('./foo.txt', {
  start: 3,
  end: 8,
  highWaterMark: 4
})

2、监听 data 事件,获取读取到的数据

js
read.on('data', (data) => {
  console.log(data)
})

监听其他的事件

js
read.on('open', (fd) => {
  console.log('文件被打开')
})

read.on('end', () => {
  console.log('文件读取结束')
})

read.on('close', () => {
  console.log('文件被关闭')
})

暂停和恢复读取

js
read.on('data', (data) => {
  console.log(data)

  read.pause()

  setTimeout(() => {
    read.resume()
  }, 2000)
})

Writable

一次性写入

之前我们写入一个文件的方式是这样的:

js
fs.writeFile('./foo.txt', '内容', (err) => {})

这种方式相当于一次性将所有的内容写入到文件中

问题:无法一点点写入内容,精确每次写入的位置等

流式写入

这个时候,我们可以使用 createWriteStream(),我们来看几个参数,更多参数可以参考官网:

  • flags:默认是w,如果我们希望是追加写入,可以使用 a或者 a+
  • start:写入的位置;

基本使用

我们进行一次简单的写入

js
const writer = fs.createWriteStream('./foo.txt', {
  flags: 'a+',
  start: 8
})

writer.write('你好啊', (err) => {
  console.log('写入成功')
})

另外一个非常常用的方法是 endend方法相当于做了两步操作:write传入的数据和调用close方法;

js
writer.end('Hello World')

监听其他事件

  • open,监听文件打开事件
  • finish:监听写入结束事件
  • close:监听文件关闭事件
js
writer.on('open', () => {
  console.log('文件打开')
})

writer.on('finish', () => {
  console.log('文件写入结束')
})

writer.on('close', () => {
  console.log('文件关闭')
})

问题:我们会发现,我们并不能监听到 close 事件

解决: 这是因为写入流在打开后是不会自动关闭的,我们必须手动关闭,来告诉 Node 已经写入结束了,并且会发出一个 finish 事件的

js
writer.close()

writer.on('finish', () => {
  console.log('文件写入结束')
})

writer.on('close', () => {
  console.log('文件关闭')
})

问题:createWriteStream()中当 flags 为'a+'时,start 参数无效

分析: 在 mac 电脑中没有问题,但 windows 中存在该问题

解决: 在 windows 中的 flags 值设置为'r+'

image-20251020150155200

pipe()

作用:将一个可读流和一个可写流连接起来,使得从可读流中读取的数据可以自动写入到可写流中

语法

js
readStream.pipe(dest, options?)

一次性读取和写入

image-20251020143834851

流式读取和写入

正常情况下,我们可以将读取到的 输入流,手动的放到 输出流中进行写入:

image-20251020144211254

js
const fs = require('fs')
const { read } = require('fs/promises')

const reader = fs.createReadStream('./foo.txt')
const writer = fs.createWriteStream('./bar.txt')

reader.on('data', (data) => {
  console.log(data)
  writer.write(data, (err) => {
    console.log(err)
  })
})

在可读、可写流之间建立管道

我们也可以通过 pipe 来完成这样的操作:

js
reader.pipe(writer)

writer.on('close', () => {
  console.log('输出流关闭')
})

http

API-http

http

http.Server

http.IncomingMesage

概述

web 服务器

概念:当应用程序(客户端)需要某一个资源时,可以向一个台服务器,通过 Http 请求获取到这个资源;提供资源的这个服务器,就是一个Web 服务器

image-20240719155622943

种类:目前有很多开源的 Web 服务器:NginxApache(静态)、Apache Tomcat(静态、动态)、Node.js

Node 中的 web 服务器:在 Node 中,主要通过 http 模块实现 web 服务器

网络请求

【TODO】

基本使用

入门案例

创建一个 Web 服务器的初体验:

js
const http = require('http')

const HTTP_PORT = 8000

const server = http.createServer((req, res) => {
  res.end('Hello World')
})

server.listen(8000, () => {
  console.log(`🚀服务器在${HTTP_PORT}启动~`)
})

创建服务器

创建服务器对象,我们是通过 createServer() 来完成的

  • http.createServer会返回服务器的对象;
  • 底层其实使用直接 new Server 对象。
js
function createServer(opts, requestListener) {
  return new Server(opts, requestListener)
}

那么,当然,我们也可以自己来创建这个对象:

js
const server2 = new http.Server((req, res) => {
  res.end('Hello Server2')
})

server2.listen(9000, () => {
  console.log('服务器启动成功~')
})

上面我们已经看到,创建 Server 时会传入一个回调函数,这个回调函数在被调用时会传入两个参数:

  • req:request 请求对象,包含请求相关的信息;
  • res:response 响应对象,包含我们要发送给客户端的信息;

启动一个 HTTP 服务器并监听指定的端口

Server 通过 listen 方法来开启服务器,并且在某一个主机和端口上监听网络请求:

  • 也就是当我们通过 ip:port的方式发送到我们监听的 Web 服务器上时;
  • 我们就可以对其进行相关的处理;

listen函数有三个参数:

  • 端口 port: 可以不传, 系统会默认分配端, 后续项目中我们会写入到环境变量中;
  • 主机 host: 通常可以传入 localhost、ip 地址 127.0.0.1、或者 ip 地址 0.0.0.0,默认是 0.0.0.0
    • localhost:本质上是一个域名,通常情况下会被解析成 127.0.0.1;
    • 127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
      • 正常的数据库包经常 应用层 - 传输层 - 网络层 - 数据链路层 - 物理层 ;
      • 而回环地址,是在网络层直接就被获取到了,是不会经常数据链路层和物理层的;
      • 比如我们监听 127.0.0.1时,在同一个网段下的主机中,通过 ip 地址是不能访问的;
    • 0.0.0.0
      • 监听 IPV4 上所有的地址,再根据端口找到不同的应用程序;
      • 比如我们监听 0.0.0.0时,在同一个网段下的主机中,通过 ip 地址是可以访问的;
  • 回调函数:服务器启动成功时的回调函数;
js
server.listen(() => {
  console.log('服务器启动~🚀')
})

request

注意:request 继承了IncomingMessageReadableEventEmitter

在向服务器发送请求时,我们会携带很多信息,比如:

  • 本次请求的 URL,服务器需要根据不同的 URL 进行不同的处理;
  • 本次请求的请求方式,比如 GET、POST 请求传入的参数和处理的方式是不同的;
  • 本次请求的 headers 中也会携带一些信息,比如客户端信息、接受数据的格式、支持的编码格式等;
  • 等等...

这些信息,Node 会帮助我们封装到一个 request 的对象中,我们可以直接来处理这个 request 对象:

js
const server = http.createServer((req, res) => {
  // request对象
  console.log(req.url)
  console.log(req.method)
  console.log(req.headers)

  res.end('Hello World')
})

url

http 请求参数方式

  • 查询字符串(query string):如https://example.com/path?key1=value1&key2=value2
  • 请求体(body)
    • 表单提交Content-Type: application/x-www-form-urlencoded
    • json 提交Content-Type: application/json
解析-query string

image-20240118115130989

1、解析 url

服务器通过req.url获取请求的 url

客户端在发送请求时,会请求不同的数据,那么会传入不同的请求地址:

  • 比如 http://localhost:8000/login
  • 比如 http://localhost:8000/products;

服务器端需要根据不同的请求地址,作出不同的响应:

js
const server = http.createServer((req, res) => {
  const url = req.url

  if (url === '/login') {
    res.end('welcome Back~')
  } else if (url === '/products') {
    res.end('products')
  } else {
    res.end('error message')
  }
})

那么如果用户发送的地址中还携带一些额外的参数呢?

  • http://localhost:8000/login?name=why&password=123;
  • 这个时候,url 的值是 /login?name=why&password=123

我们如何对它进行解析呢?使用内置模块 url

  • url.parse()
js
const url = require('url')

// 解析请求
const parseInfo = url.parse(req.url)
console.log(parseInfo)

解析结果:

js
Url {
  protocol: null,
  slashes: null,
  auth: null,
  host: null,
  port: null,
  hostname: null,
  hash: null,
  search: '?name=why&password=123',
  query: 'name=why&password=123',
  pathname: '/login',
  path: '/login?name=why&password=123',
  href: '/login?name=why&password=123'
}

2、解析 query

我们会发现 pathname就是我们想要的结果。

但是 query 信息如何可以获取呢?

  • 方式一:截取字符串;
  • 方式二:使用 querystring 内置模块;
js
const { pathname, query } = url.parse(req.url)
const queryObj = qs.parse(query)
console.log(queryObj.name)
console.log(queryObj.password)
解析-body

image-20240118115142205

method

请求方式

在 Restful 规范(设计风格)中,我们对于数据的增删改查应该通过不同的请求方式:

  • GET:查询数据
  • POST:新建数据
  • PATCH:更新数据
  • DELETE:删除数据
  • PUT

所以,我们可以通过判断不同的请求方式进行不同的处理。

比如创建一个用户:

  • 请求接口为 /users
  • 请求方式为 POST请求;
  • 携带数据 usernamepassword

创建用户请求

image-20240719155652679

在我们程序中如何进行判断以及获取对应的数据呢?

  • 这里我们需要判断接口是 /users,并且请求方式是 POST 方法去获取传入的数据;
  • 获取这种 body 携带的数据,我们需要通过监听 req 的 data事件来获取;
js
if (req.url.indexOf('/users') !== -1) {
  if (req.method === 'POST') {
    // 可以设置编码,也可以在下方通过 data.toString() 获取字符串格式
    req.setEncoding('utf-8')

    req.on('data', (data) => {
      const { username, password } = JSON.parse(data)
      console.log(username, password)
    })

    res.end('create user success')
  } else {
    res.end('users list')
  }
} else {
  res.end('error message')
}

将 JSON 字符串格式转成对象类型,通过JSON.parse方法即可。

headers

常见 headers

  • content-type:``,这次请求携带的数据的类型
    • application/json:``,json 类型
    • application/x-www-form-urlencoded:``,表单类型
    • text/plain:``,文本类型
    • application/xml:``,xml 类型
    • multipart/form-data:``,上传文件
  • content-length:``,文件的大小和长度
  • connection: 'keep-alive':``,保持连接,默认 5s
  • accept,客户端可接受文件的格式类型,默认*/*
  • accept-encoding,客户端支持的文件压缩格式,默认gzip, deflate, br
  • user-agent:``,客户端相关的信息
  • authorization:``,自定义 token

在 request 对象的 header 中也包含很多有用的信息:

js
const server = http.createServer((req, res) => {
  console.log(req.headers)

  res.end('Hello Header')
})

浏览器会默认传递过来一些信息:

json
{
  "content-type": "application/json",
  "user-agent": "PostmanRuntime/7.26.5",
  "accept": "*/*",
  "postman-token": "afe4b8fe-67e3-49cc-bd6f-f61c95c4367b",
  "host": "localhost:8000",
  "accept-encoding": "gzip, deflate, br",
  "connection": "keep-alive",
  "content-length": "48"
}

response

注意:response 继承了IncomingMessageWritableEventEmitter

响应结果

如果我们希望给客户端响应的结果数据,可以通过两种方式:

  • response.end():写出数据,并关闭流
  • response.write():写出数据,不关闭流
js
const http = require('http')

const server = http.createServer((req, res) => {
  // 响应数据的方式有两个:
  res.write('Hello World')
  res.write('Hello Response')
  res.end('message end')
})

server.listen(8000, () => {
  console.log('服务器启动🚀~')
})

如果我们没有调用 end,客户端将会一直等待结果,所以客户端在发送网络请求时,都会设置超时时间。

响应状态码

设置状态码

  • response.statusCode:``,设置 HTTP 状态码
  • response.writeHead():``,设置 HTTP 状态码

常见状态码

  • 200OK,客户端请求成功
  • 201Created,POST 请求,创建新的资源
  • 301Moved Permanently,请求资源的 URL 已永久更改。在响应中给出了新的 URL。
  • 400Bad Request,客户端错误,服务器无法或不会处理请求
  • 401Unauthorized,未授权的错误,必须携带请求的身份信息
  • 403Forbidden,客户端没有权限访问,被拒接
  • 404Not Found,服务器找不到请求的资源
  • 500Internal Server Error,服务器遇到了不知道如何处理的情况
  • 503Bad Gateway,服务器不可用,可能处理维护或者重载状态,暂时无法访问

响应 Header

设置响应 Header

  • res.setHeader():单独设置某个 Header
  • res.writeHead():和状态码一起设置
js
res.setHeader('Content-Type', 'application/json;charset=utf8')

res.writeHead(200, {
  'Content-Type': 'application/json;charset=utf8'
})

Content-Type 作用

Header 设置 Content-Type有什么作用呢?

  • 默认客户端接收到的是字符串,客户端会按照自己默认的方式进行处理;

比如,我们返回的是一段 HTML,但是没有指定格式:

js
res.end('<h2>Hello World</h2>')

image-20240719155810001

但是,如果我们指定了格式:

js
res.setHeader('Content-Type', 'text/html;charset=utf8')
res.end('<h2>Hello World</h2>')

image-20240719155824952

如果我们希望返回一段 JSON 数据,应该怎么做呢?

js
res.writeHead(200, {
  'Content-Type': 'application/json;charset=utf8'
})

const data = {
  name: '王红元',
  age: 18,
  height: 1.88
}

res.end(JSON.stringify(data))

网络请求

axios 实现原理

axios 库可以在浏览器中使用,也可以在 Node 中使用

  • 浏览器:通过封装 XHR 和 fetch 实现的网络请求
  • Node:通过封装 http 模块实现的网络请求

http.get()

发送 get 请求:

js
http.get('http://localhost:8000', (res) => {
  res.on('data', (data) => {
    console.log(data.toString())
    console.log(JSON.parse(data.toString()))
  })
})

http.request()

发送 post 请求:

注意:必须调用req.end(),表示写入内容完成

js
const req = http.request(
  {
    method: 'POST',
    hostname: 'localhost',
    port: 8000
  },
  (res) => {
    res.on('data', (data) => {
      console.log(data.toString())
      console.log(JSON.parse(data.toString()))
    })
  }
)

req.on('error', (err) => {
  console.log(err)
})

req.end()

文件上传

上传图片

注意

  • 文件上传必须是 POST 请求
  • 文件上传 body 格式必须是 multipart/form-data
postman 上传

image-20240118121325931

浏览器上传

注意

  • 携带 headers:发送 formData 时,需要设置 headers 为 { 'Content-Type': 'multipart/form-data'}

image-20240118121345487

接收图片

错误的做法

如果是一个很大的文件需要上传到服务器端,服务器端进行保存应该如何操作呢?

js
const server = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      // 创建可写流,并写入数据
      const fileWriter = fs.createWriteStream('./foo.png', {flags: 'a+'})
      req.pipe(fileWriter)

      // 计算文件上传进度
      const fileSize = req.headers['content-length']
      let curSize = 0
      console.log(fileSize)
      req.on('data', (data) => {
        curSize += data.length
        console.log(curSize)
        res.write(`文件上传进度: ${(curSize / fileSize) * 100}%\n`)
      })

      // 文件上传完成
      req.on('end', () => {
        res.end('文件上传完成~')
      })
    }
  } else {
    res.end('error message')
  }
})
正确的做法@

这个时候我们发现文件上传成功了,但是文件却打不开:

  • 这是因为我们写入的数据,里面包含一些特殊的信息;
  • 这些信息打开的软件并不能很好的解析;

文件上传的数据:

image-20251021162120685

js
const server = http.createServer((req, res) => {
  if (req.url === '/upload') {
    if (req.method === 'POST') {
      // 1. 图片文件必须设置为二进制的
      req.setEncoding('binary')

      // 2. 获取content-type中的boundary的值
      var boundary = req.headers['content-type'].split('; ')[1].replace('boundary=', '')

      // 记录当前数据的信息
      const fileSize = req.headers['content-length']
      let curSize = 0
      let body = ''

      // 监听当前的数据
      req.on('data', (data) => {
        curSize += data.length
        res.write(`文件上传进度: ${(curSize / fileSize) * 100}%\n`)
        body += data
      })

      // 数据结构
      req.on('end', () => {
        // 3. 切割出图片部分的数据
        const payload = qs.parse(body, '\r\n', ':')
        // 获取最后的类型(image/png)
        const fileType = payload['Content-Type'].substring(1)
        // 获取要截取的长度
        const fileTypePosition = body.indexOf(fileType) + fileType.length
        let binaryData = body.substring(fileTypePosition)
        binaryData = binaryData.replace(/^\s\s*/, '')
		// 去除末尾的 boundary
        // binaryData = binaryData.replaceAll('\r\n', '');
        const finalData = binaryData.substring(0, binaryData.indexOf('--' + boundary + '--'))

        // 4. 存储图片数据到文件中(需指定编码为:binary)
        fs.writeFile('./boo.png', finalData, 'binary', (err) => {
          console.log(err)
          res.end('文件上传完成~')
        })
      })
    }
  } else {
    res.end('error message')
  }
})

url

API-url

url

  • url.parse()(urlString, parseQueryString?, slashesDenoteHost?)废弃,用于解析 URL 字符串,并返回一个包含各个组成部分的 URL 对象。
  • url.fileURLToPath()( url ),用于将 file 协议的 URL 转换为对应的文件系统路径。

URL

属性

  • 与 Location 属性一致
  • hrefhttps://example.com:8080/path?q=1#hash,获取或设置完整的 URL。修改会触发页面跳转。
  • protocolhttps:,URL 的协议(含 :)。修改可能触发跳转。
  • hostnameexample.com,域名部分。修改会跳转到新域名。
  • port8080,端口号(字符串形式)。若无则为空。修改会跳转到新端口。
  • hostexample.com:8080,域名 + 端口(若端口非默认)。修改会跳转。
  • pathname/path/,路径部分(以 / 开头)。修改会跳转到新路径。
  • search?q=1,查询字符串(含 ?)。修改会重新加载页面。
  • hash#hash,哈希部分(含 #),用于页面锚点。修改不会重载页面,但会记录历史。
  • originhttps://example.com:8080只读,协议 + 域名 + 端口。
  • 特有属性
  • url.searchParamsURLSearchParams,返回专门用于操作 URL 查询参数的 URLSearchParams 对象(即 ? 后的键值对)。

方法

  • new URL()(urlString,baseUrlString?),用于创建并解析一个 URL 对象,提供对 URL 各组成部分的标准化访问和操作。
  • URL.createObjectURL()(object),用于为 BlobFile 对象生成临时 URL 的方法,允许在浏览器中直接引用本地文件或内存中的数据(如图片、视频、二进制流等)。
  • URL.revokeObjectURL()(objectURL),用于释放由 URL.createObjectURL() 生成的临时 URL 所占用的内存,避免内存泄漏。

URLSearchParams

方法

  • new URLSearchParams()(init?),创建一个专门用于操作 URL 的查询参数的实例。
  • params.append()(key, value),用于向 URL 的查询参数中添加一个新的键值对,即使键已存在也不会覆盖原有值,而是追加新的值。
  • params.delete()(key),用于删除 URL 查询参数中指定键的所有值
  • params.get()(key),用于从 URL 的查询参数中获取指定键的第一个值
  • params.set()(key,value),用于设置指定键的查询参数值。若键已存在,则覆盖其所有值;若不存在,则添加新的键值对。
  • params.has()(key),用于检查 URL 的查询参数中是否存在指定键
  • params.getAll()(key),用于从 URL 的查询参数中获取指定键的所有值(以数组形式返回)。
  • params.sort()(),用于按键名对 URL 的查询参数进行排序,并原地修改参数列表。
  • params.toString()(),用于将查询参数序列化为 URL 编码的字符串(即 ? 后的键值对部分)。
  • params.keys()(),用于获取 URL 查询参数中所有键的迭代器。它允许你遍历查询字符串中的所有键名。
  • params.values()(),用于获取 URL 查询参数中所有值的迭代器。它允许你遍历查询字符串中的所有参数值。
  • params.entries()(),用于获取 URL 查询参数中所有键值对的迭代器。它允许遍历查询字符串中的所有参数,并以 [key, value] 数组形式返回每个键值对。
  • params.forEach()(callback,thisArg?),用于遍历 URL 查询参数中的所有键值对,并对每个键值对执行回调函数。

querystring

API-querystring

方法

child_process

API-child_process

方法

util

API-util

方法

  • util.promisify()(originalFunction),用于将遵循回调模式的异步函数转换为返回 Promise 的函数。

glob

API-glob

方法

  • glob()(pattern, options?, callback),(异步),查找与指定模式匹配的文件。
  • glob.sync()(pattern, options?),(同步),查找与指定模式匹配的文件。

概述

glob:是一个用于文件路径匹配的 Node.js 库,其主要 API 用于查找与指定模式匹配的文件。

安装:glob不是node内置的库,需要另外安装

sh
pnpm i glob

glob VS require.context()

  • glob适用于任何Node.js环境,是一个用于匹配文件路径模式的库,通常在构建时使用,比如在Node.js环境下进行文件查找;
  • require.context():是Webpack特有的功能,用于动态导入模块,适用于在Webpack打包时提供更直接的模块导入方式。

glob匹配规则

  • *:匹配任意数量的字符,但不包括 /

    js
    *.js
  • **:匹配任意数量的字符,包括 /。通常用于匹配目录下的文件。

    js
    scripts/**/*.js
  • ?:匹配一个字符,但不包括 /

    js
    scripts/**/*.jsx?
  • !:取反,用于排除不匹配的文件或目录。相当于glob第二个参数options.ignore

    • 由于 glob 匹配时是按照每个 glob 在数组中的位置依次进行匹配操作的;

    • 所以 glob 数组中的取反(negative)glob 必须跟在一个非取反(non-negative)的 glob 后面;

    • 第一个 glob 匹配到一组匹配项,然后后面的取反 glob 删除这些匹配项中的一部分;

    js
    // 表示匹配scripts目录下的所有js文件,但排除scripts/vendor/目录下的文件
    ['scripts/**/*.js', '!scripts/vendor/']

示例:glob() 匹配src目录下的所有文件(包括子目录中的文件)

js
const glob = require('glob');

glob('src/**/*', (err, files) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(files);
});

示例:glob() 匹配src目录下的所有文件(不包括目录)

js
const glob = require('glob');

glob('src/**/*', { nodir: true }, (err, files) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(files);
});

示例:glob.sync() 同步匹配所有js文件

js
const glob = require('glob');

const files = glob.sync('**/*.js');
console.log(files);